﻿using System;
using System.Linq.Expressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Postnuclear.Dependency;
using Postnuclear.Test.Helpers;

namespace Postnuclear.Test.Dependency
  {
  /// <summary>
  /// Represents a test class for the <see cref="Expressions"/> class.
  /// </summary>
  [TestClass]
  public sealed class ExpressionsTest
    : TestClassBase
    {
    #region Public

    // Methods

    /// <summary>
    /// A test for <see cref="Expressions.Observe"/> method main functionality.
    /// </summary>
    [TestMethod]
    public void ObserveChangesTest()
      {
      int counter = 0;
      EventHandler<EventArgs> handler = (_, __) => ++counter;

      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      provider.SetAndRaise(new object());

      Expression<Func<object>> expression = () => provider.Object.Value;
      INotifyValueChanged observer = expression.Body.Observe();

      provider.SetAndRaise(new object());
      Assert.AreEqual(0, counter);

      observer.ValueChanged += handler;
      Assert.AreEqual(0, counter);

      provider.SetAndRaise(new object());
      Assert.AreEqual(1, counter);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Observe"/> method garbage collection functionality.
    /// </summary>
    [TestMethod]
    public void ObserveChangesGarbageCollectionTest()
      {
      int counter = 0;
      EventHandler<EventArgs> handler = (_, __) => ++counter;
      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      Expression<Func<object>> expression = () => provider.Object.Value;
      INotifyValueChanged observer = expression.Body.Observe();
      WeakReference<INotifyValueChanged> valueProviderReference = new WeakReference<INotifyValueChanged>(observer);
      observer.ValueChanged += handler;

      Assert.IsTrue(valueProviderReference.IsAlive);

      GC.KeepAlive(observer);
      observer = null;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsTrue(valueProviderReference.IsAlive);

      valueProviderReference.Target.ValueChanged -= handler;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsFalse(valueProviderReference.IsAlive);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Changed"/> method main functionality.
    /// </summary>
    [TestMethod]
    public void OnChangedTest()
      {
      int count = 0;
      EventHandler<ValueEventArgs<object>> handler = (_, __) => ++count;

      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      provider.SetAndRaise(new object());

      Expressions.Changed(() => provider.Object.Value, handler);
      Assert.AreEqual(0, count);

      provider.SetAndRaise(new object());
      Assert.AreEqual(1, count);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Changed"/> method garbage collection functionality.
    /// </summary>
    [TestMethod]
    public void OnChangedGarbageCollectionTest()
      {
      int count = 0;
      Mock<IHandlerProvider<EventArgs>> handler = new Mock<IHandlerProvider<EventArgs>>();
      handler.Setup(m => m.Handle(It.IsAny<object>(), It.IsAny<EventArgs>())).Callback(() => ++count);

      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      provider.SetAndRaise(new object());

      Expression<Func<object>> expression = () => provider.Object.Value;
      expression.Body.Changed(handler.Object.Handle);
      Assert.AreEqual(0, count);

      provider.SetAndRaise(new object());
      Assert.AreEqual(1, count);

      GC.KeepAlive(handler);
      handler = null;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      provider.SetAndRaise(new object());
      Assert.AreEqual(1, count);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Observe{TValue}"/> method main functionality.
    /// </summary>
    [TestMethod]
    public void ObserveValueTest()
      {
      int counter = 0;
      EventHandler<EventArgs> handler = (_, __) => ++counter;

      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      provider.SetAndRaise(new object());

      IValueProvider<object> valueProvider = Expressions.Observe(() => provider.Object.Value);
      Assert.AreEqual(provider.Object.Value, valueProvider.Value);

      provider.SetAndRaise(new object());
      Assert.AreNotEqual(provider.Object.Value, valueProvider.Value);

      valueProvider.ValueChanged += handler;
      Assert.AreEqual(provider.Object.Value, valueProvider.Value);
      Assert.AreEqual(0, counter);

      provider.SetAndRaise(new object());
      Assert.AreEqual(provider.Object.Value, valueProvider.Value);
      Assert.AreEqual(1, counter);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Observe{TValue}"/> method garbage collection functionality.
    /// </summary>
    [TestMethod]
    public void ObserveValueGarbageCollectionTest()
      {
      int counter = 0;
      EventHandler<EventArgs> handler = (_, __) => ++counter;
      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      IValueProvider<object> valueProvider = Expressions.Observe(() => provider.Object.Value);
      WeakReference<IValueProvider<object>> valueProviderReference = new WeakReference<IValueProvider<object>>(valueProvider);
      valueProvider.ValueChanged += handler;

      Assert.IsTrue(valueProviderReference.IsAlive);

      GC.KeepAlive(valueProvider);
      valueProvider = null;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsTrue(valueProviderReference.IsAlive);

      valueProviderReference.Target.ValueChanged -= handler;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      Assert.IsFalse(valueProviderReference.IsAlive);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Changed{TValue}"/> method main functionality.
    /// </summary>
    [TestMethod]
    public void OnValueChangedTest()
      {
      object result = null;
      EventHandler<ValueEventArgs<object>> handler = (_, e) => result = e.Value;

      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      provider.SetAndRaise(new object());

      Expressions.Changed(() => provider.Object.Value, handler);
      Assert.AreNotEqual(provider.Object.Value, result);

      provider.SetAndRaise(new object());
      Assert.AreEqual(provider.Object.Value, result);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Changed{TValue}"/> method garbage collection functionality.
    /// </summary>
    [TestMethod]
    public void OnValueChangedGarbageCollectionTest()
      {
      int count = 0;
      Mock<IHandlerProvider<ValueEventArgs<object>>> handler = new Mock<IHandlerProvider<ValueEventArgs<object>>>();
      handler.Setup(m => m.Handle(It.IsAny<object>(), It.IsAny<ValueEventArgs<object>>())).Callback(() => ++count);

      Mock<IValueProvider<object>> provider = new Mock<IValueProvider<object>>();
      provider.SetAndRaise(new object());

      Expressions.Changed(() => provider.Object.Value, handler.Object.Handle);
      Assert.AreEqual(0, count);

      provider.SetAndRaise(new object());
      Assert.AreEqual(1, count);

      GC.KeepAlive(handler);
      handler = null;
      GarbageCollector.CollectAndWaitForPendingFinalizers();
      provider.SetAndRaise(new object());
      Assert.AreEqual(1, count);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Bind{TMember, TValue}"/> method to check exception for target null expression.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(ArgumentNullException))]
    public void BindWithNullTargetTest()
      {
      Expressions.Bind<object, object>(null, () => TestContext);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Bind{TMember, TValue}"/> method to check exception for source null expression.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(ArgumentNullException))]
    public void BindWithNullSourceTest()
      {
      Expressions.Bind<object, object>(() => TestContext, null);
      }

    /// <summary>
    /// A test for <see cref="Expressions.Bind{TMember, TValue}"/> method to check exception when target is not a member access expression.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void BindWithNonMemberAccessTargetTest()
      {
      int target = 0;
      Expressions.Bind(() => target + target, () => target);
      }

    /// <summary>
    /// A test for main binding functionality.
    /// </summary>
    [TestMethod]
    public void BindingTest()
      {
      Target = 10;
      Mock<IValueProvider<int>> expectedResult = new Mock<IValueProvider<int>>();
      expectedResult.SetAndRaise(Target + 1);

      Expressions.Bind(() => Target, () => expectedResult.Object.Value);
      Assert.AreEqual(expectedResult.Object.Value, Target);

      expectedResult.SetAndRaise(Target + 1);
      Assert.AreEqual(expectedResult.Object.Value, Target);
      }

    /// <summary>
    /// A test for main binding functionality with defaulting.
    /// </summary>
    [TestMethod]
    public void BindingWithDefaultingTest()
      {
      Target = 10;

      Expressions.Bind(() => Target, () => Throw());
      Assert.AreEqual(default(int), Target);
      }

    /// <summary>
    /// A test for main binding functionality without defaulting.
    /// </summary>
    [TestMethod]
    [ExpectedException(typeof(DivideByZeroException))]
    public void BindingWithoutDefaultingTest()
      {
      Expressions.Bind(() => Target, () => Throw(), false);
      }

    #endregion

    #region Private

    // Properties

    private int Target
      {
      get;
      set;
      }

    // Methods

    private int Throw()
      {
      throw new DivideByZeroException();
      }

    #endregion
    }
  }